/*
 * Copyright 2019 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.uberfire.java.nio.fs.file;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.net.URI;
import java.nio.channels.FileChannel;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.concurrent.ExecutorService;

import org.apache.commons.io.FileUtils;
import org.uberfire.java.nio.IOException;
import org.uberfire.java.nio.base.BasicFileAttributesImpl;
import org.uberfire.java.nio.base.ExtendedAttributeView;
import org.uberfire.java.nio.base.GeneralPathImpl;
import org.uberfire.java.nio.base.SeekableByteChannelFileBasedImpl;
import org.uberfire.java.nio.channels.AsynchronousFileChannel;
import org.uberfire.java.nio.channels.SeekableByteChannel;
import org.uberfire.java.nio.file.AccessDeniedException;
import org.uberfire.java.nio.file.AccessMode;
import org.uberfire.java.nio.file.AtomicMoveNotSupportedException;
import org.uberfire.java.nio.file.CopyOption;
import org.uberfire.java.nio.file.DeleteOption;
import org.uberfire.java.nio.file.DirectoryNotEmptyException;
import org.uberfire.java.nio.file.DirectoryStream;
import org.uberfire.java.nio.file.FileAlreadyExistsException;
import org.uberfire.java.nio.file.FileStore;
import org.uberfire.java.nio.file.FileSystem;
import org.uberfire.java.nio.file.FileSystemAlreadyExistsException;
import org.uberfire.java.nio.file.FileSystemNotFoundException;
import org.uberfire.java.nio.file.LinkOption;
import org.uberfire.java.nio.file.NoSuchFileException;
import org.uberfire.java.nio.file.NotDirectoryException;
import org.uberfire.java.nio.file.NotLinkException;
import org.uberfire.java.nio.file.OpenOption;
import org.uberfire.java.nio.file.Path;
import org.uberfire.java.nio.file.StandardDeleteOption;
import org.uberfire.java.nio.file.attribute.BasicFileAttributeView;
import org.uberfire.java.nio.file.attribute.BasicFileAttributes;
import org.uberfire.java.nio.file.attribute.FileAttribute;
import org.uberfire.java.nio.file.attribute.FileAttributeView;
import org.uberfire.java.nio.file.spi.FileSystemProvider;

import static org.kie.soup.commons.validation.Preconditions.checkCondition;
import static org.kie.soup.commons.validation.Preconditions.checkNotEmpty;
import static org.kie.soup.commons.validation.Preconditions.checkNotNull;
import static org.uberfire.java.nio.file.StandardOpenOption.READ;
import static org.uberfire.java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;

public class SimpleFileSystemProvider implements FileSystemProvider {

    private static final String STREAM_CLOSED = "This stream is closed.";
    private static final String USER_DIR = "user.dir";
    protected BaseSimpleFileSystem fileSystem;
    private final OSType osType;
    private final File[] roots;
    private boolean isDefault;

    public SimpleFileSystemProvider() {
        this(File.listRoots(),
             OSType.currentOS());
    }

    protected SimpleFileSystemProvider(final File[] roots,
                                       final OSType osType) {
        final String defaultPath = System.getProperty(USER_DIR);
        this.osType = checkNotNull("osType", osType);

        if (osType == OSType.WINDOWS) {
            this.roots = checkNotNull("roots", roots);
            this.fileSystem = new SimpleWindowsFileSystem(this, defaultPath);
        } else {
            this.roots = roots;
            if (roots != null) {
                this.fileSystem = new SimpleUnixFileSystem(this, defaultPath);
            }
        }
    }

    @Override
    public synchronized void forceAsDefault() {
        this.isDefault = true;
    }

    @Override
    public boolean isDefault() {
        return isDefault;
    }

    @Override
    public String getScheme() {
        return "file";
    }

    @Override
    public FileSystem getFileSystem(final URI uri)
            throws IllegalArgumentException, FileSystemNotFoundException, SecurityException {
        return getDefaultFileSystem();
    }

    @Override
    public Path getPath(final URI uri)
            throws IllegalArgumentException, FileSystemNotFoundException, SecurityException {
        checkNotNull("uri",
                     uri);
        checkCondition("uri scheme not supported",
                       uri.getScheme().equals(getScheme()) || uri.getScheme().equals("default"));

        return getDefaultFileSystem().getPath(uri.getPath());
    }

    @Override
    public FileSystem newFileSystem(final URI uri,
                                    final Map<String, ?> env)
            throws IllegalArgumentException, IOException, SecurityException, FileSystemAlreadyExistsException {
        checkNotNull("uri",
                     uri);
        checkNotNull("env",
                     env);
        throw new FileSystemAlreadyExistsException(uri.toString());
    }

    @Override
    public FileSystem newFileSystem(final Path path,
                                    final Map<String, ?> env)
            throws IllegalArgumentException, UnsupportedOperationException, IOException, SecurityException {
        checkNotNull("path",
                     path);
        checkNotNull("env",
                     env);
        throw new FileSystemAlreadyExistsException(path.toString());
    }

    @Override
    public InputStream newInputStream(final Path path,
                                      final OpenOption... options)
            throws IllegalArgumentException, NoSuchFileException, IOException, SecurityException {
        checkNotNull("path",
                     path);
        final File file = path.toFile();
        if (!file.exists()) {
            throw new NoSuchFileException(file.toString());
        }
        try {
            return new FileInputStream(path.toFile());
        } catch (FileNotFoundException e) {
            throw new NoSuchFileException(e.getMessage());
        }
    }

    @Override
    public OutputStream newOutputStream(final Path path,
                                        final OpenOption... options)
            throws IllegalArgumentException, UnsupportedOperationException, IOException, SecurityException {
        checkNotNull("path",
                     path);
        try {
            return new FileOutputStream(path.toFile());
        } catch (Exception e) {
            throw new IOException("Could not open output stream.",
                                  e);
        }
    }

    @Override
    public FileChannel newFileChannel(final Path path,
                                      final Set<? extends OpenOption> options,
                                      final FileAttribute<?>... attrs)
            throws IllegalArgumentException, UnsupportedOperationException, IOException, SecurityException {
        checkNotNull("path",
                     path);
        return ((FileOutputStream) newOutputStream(path)).getChannel();
    }

    @Override
    public AsynchronousFileChannel newAsynchronousFileChannel(final Path path,
                                                              final Set<? extends OpenOption> options,
                                                              final ExecutorService executor,
                                                              FileAttribute<?>... attrs)
            throws IllegalArgumentException, UnsupportedOperationException, IOException, SecurityException {
        checkNotNull("path",
                     path);
        throw new UnsupportedOperationException();
    }

    @Override
    public SeekableByteChannel newByteChannel(final Path path,
                                              final Set<? extends OpenOption> options,
                                              final FileAttribute<?>... attrs)
            throws IllegalArgumentException, UnsupportedOperationException, FileAlreadyExistsException, IOException, SecurityException {
        final File file = checkNotNull("path",
                                       path).toFile();

        if (file.exists()) {
            if (!shouldCreateOrOpenAByteChannel(options)) {
                throw new FileAlreadyExistsException(path.toString());
            }
        }

        try {
            if (options != null && options.contains(READ)) {
                return openAByteChannel(path);
            } else {
                return createANewByteChannel(file);
            }
        } catch (java.io.IOException e) {
            throw new IOException("Failed to open or create a byte channel.",
                                  e);
        }
    }

    private SeekableByteChannelFileBasedImpl createANewByteChannel(final File file) throws FileNotFoundException {
        return new SeekableByteChannelFileBasedImpl(new RandomAccessFile(file,
                                                                         "rw").getChannel()) {
            @Override
            public void close() throws java.io.IOException {
                super.close();
            }
        };
    }

    private SeekableByteChannelFileBasedImpl openAByteChannel(Path path) throws FileNotFoundException {
        return new SeekableByteChannelFileBasedImpl(new RandomAccessFile(path.toFile(),
                                                                         "r").getChannel());
    }

    private boolean shouldCreateOrOpenAByteChannel(final Set<? extends OpenOption> options) {
        return (options != null && (options.contains(TRUNCATE_EXISTING) || options.contains(READ)));
    }

    @Override
    public void createDirectory(final Path dir,
                                final FileAttribute<?>... attrs)
            throws UnsupportedOperationException, FileAlreadyExistsException, IOException, SecurityException {
        checkNotNull("dir",
                     dir);
        final Path realDir = dir.toAbsolutePath();
        checkFileExistsThenThrow(realDir);
        realDir.toFile().mkdirs();
    }

    @Override
    public DirectoryStream<Path> newDirectoryStream(final Path dir,
                                                    final DirectoryStream.Filter<Path> filter)
            throws NotDirectoryException, IOException, SecurityException {
        checkNotNull("filter", filter);
        final Path[] content = getDirectoryContent(dir);
        return new DirectoryStream<Path>() {
            boolean isClosed = false;

            @Override
            public void close() throws IOException {
                if (isClosed) {
                    throw new IOException(STREAM_CLOSED);
                }
                isClosed = true;
            }

            @Override
            public Iterator<Path> iterator() {
                if (isClosed) {
                    throw new IOException(STREAM_CLOSED);
                }
                return new Iterator<Path>() {
                    public boolean atEof = false;
                    private int i = -1;
                    private Path nextEntry = null;

                    @Override
                    public boolean hasNext() {
                        if (isClosed) {
                            throw new IOException(STREAM_CLOSED);
                        }
                        if (nextEntry == null && !atEof) {
                            nextEntry = readNextEntry();
                        }
                        return nextEntry != null;
                    }

                    @Override
                    public Path next() {
                        final Path result;
                        if (isClosed) {
                            throw new IOException(STREAM_CLOSED);
                        }
                        if (nextEntry == null && !atEof) {
                            result = readNextEntry();
                        } else {
                            result = nextEntry;
                            nextEntry = null;
                        }
                        if (result == null) {
                            throw new NoSuchElementException();
                        }
                        return result;
                    }

                    private Path readNextEntry() {
                        if (atEof) {
                            return null;
                        }

                        Path result = null;
                        while (true) {
                            i++;
                            if (i >= content.length) {
                                atEof = true;
                                break;
                            }

                            final Path path = content[i];
                            if (filter.accept(path)) {
                                result = path;
                                break;
                            }
                        }

                        return result;
                    }

                    @Override
                    public void remove() {
                        throw new UnsupportedOperationException();
                    }
                };
            }
        };
    }

    protected Path[] getDirectoryContent(final Path dir) {
        final File file = checkNotNull("dir", dir).toFile();
        if (!file.isDirectory()) {
            throw new NotDirectoryException(dir.toString());
        }
    
        final File[] content = file.listFiles();
        if (content == null) {
            throw new NotDirectoryException(dir.toString());
        }
    
        return Arrays.stream(content)
                     .map(fileItem -> GeneralPathImpl.newFromFile(getDefaultFileSystem(), fileItem))
                     .toArray(Path[]::new);
    }

    @Override
    public void createSymbolicLink(final Path link,
                                   final Path target,
                                   final FileAttribute<?>... attrs)
            throws UnsupportedOperationException, FileAlreadyExistsException, IOException, SecurityException {
        checkNotNull("link",
                     link);
        checkNotNull("target",
                     target);
        checkCondition("link and target can't be same",
                       !link.equals(target));
        checkCondition("target must already exists",
                       target.toFile().exists());

        checkFileExistsThenThrow(link);

        throw new UnsupportedOperationException();
    }

    @Override
    public void createLink(final Path link,
                           final Path existing)
            throws UnsupportedOperationException, FileAlreadyExistsException, IOException, SecurityException {
        checkNotNull("link",
                     link);
        checkNotNull("existing",
                     existing);
        checkCondition("existing must already exists",
                       existing.toFile().exists());
        checkCondition("link and target can't be same",
                       !link.equals(existing));

        checkFileExistsThenThrow(link);

        throw new UnsupportedOperationException();
    }

    @Override
    public void delete(final Path path,
                       final DeleteOption... options) throws NoSuchFileException, DirectoryNotEmptyException, IOException, SecurityException {
        checkNotNull("path",
                     path);

        checkFileNotExistThenThrow(path, false);

        deleteIfExists(path,
                       options);
    }

    @Override
    public boolean deleteIfExists(final Path path,
                                  final DeleteOption... options)
            throws DirectoryNotEmptyException, IOException, SecurityException {
        checkNotNull("path",
                     path);
        synchronized (this) {

            final File file = path.toFile();
            try {
                if (file.isDirectory() && !deleteNonEmptyDirectory(options) && file.list().length > 0) {
                    throw new DirectoryNotEmptyException(path.toString());
                }

                final boolean result = file.exists();

                try {
                    FileUtils.forceDelete(file);
                } catch (final FileNotFoundException ignore) {
                } catch (java.io.IOException e) {
                    throw new IOException(e);
                }

                return result;
            } finally {
                toGeneralPathImpl(path).clearCache();
            }
        }
    }

    private boolean deleteNonEmptyDirectory(final DeleteOption... options) {

        for (final DeleteOption option : options) {
            if (option.equals(StandardDeleteOption.NON_EMPTY_DIRECTORIES)) {
                return true;
            }
        }

        return false;
    }

    @Override
    public Path readSymbolicLink(final Path link)
            throws UnsupportedOperationException, NotLinkException, IOException, SecurityException {
        checkNotNull("link",
                     link);

        checkFileNotExistThenThrow(link, true);

        throw new UnsupportedOperationException();
    }

    @Override
    public boolean isSameFile(final Path path,
                              final Path path2)
            throws IOException, SecurityException {
        checkNotNull("path",
                     path);
        checkNotNull("path2",
                     path2);

        return path.equals(path2);
    }

    @Override
    public boolean isHidden(final Path path) throws IllegalArgumentException, IOException, SecurityException {
        checkNotNull("path",
                     path);

        return path.toFile().isHidden();
    }

    @Override
    public void checkAccess(final Path path,
                            AccessMode... modes)
            throws UnsupportedOperationException, NoSuchFileException, AccessDeniedException, IOException, SecurityException {
        checkNotNull("path",
                     path);
        checkNotNull("modes",
                     modes);

        checkFileNotExistThenThrow(path, false);

        if (path.toFile() != null) {
            for (final AccessMode mode : modes) {
                checkNotNull("mode",
                             mode);
                switch (mode) {
                    case READ:
                        if (!path.toFile().canRead()) {
                            throw new AccessDeniedException(path.toString());
                        }
                        break;
                    case EXECUTE:
                        if (!path.toFile().canExecute()) {
                            throw new AccessDeniedException(path.toString());
                        }
                        break;
                    case WRITE:
                        if (!path.toFile().canWrite()) {
                            throw new AccessDeniedException(path.toString());
                        }
                        break;
                }
            }
        }
    }

    @Override
    public FileStore getFileStore(final Path path) throws IOException, SecurityException {
        checkNotNull("path",
                     path);
        if (osType == OSType.WINDOWS) {
            return new SimpleWindowsFileStore(roots,
                                              path);
        }
        return new SimpleUnixFileStore(path);
    }

    @Override
    public <V extends FileAttributeView> V getFileAttributeView(final Path path,
                                                                final Class<V> type,
                                                                final LinkOption... options)
            throws NoSuchFileException {
        checkNotNull("path", path);
        checkNotNull("type", type);
        checkFileNotExistThenThrow(path, false);

        final GeneralPathImpl gPath = toGeneralPathImpl(path);

        final V view = gPath.getAttrView(type);
        if (view == null) {
            return createFileAttributeView(gPath, type);
        } else {
            return view;
        }
    }

    protected <V extends FileAttributeView> V createFileAttributeView(final GeneralPathImpl path,
                                                                      final Class<V> type) {
        if (type == BasicFileAttributeView.class || type == SimpleBasicFileAttributeView.class) {
            final V newView = (V) new SimpleBasicFileAttributeView(path);
            path.addAttrView(newView);
            return newView;
        } else {
            return null;
        }
    }

    private ExtendedAttributeView getFileAttributeView(final Path path,
                                                       final String name,
                                                       final LinkOption... options) {
        final GeneralPathImpl gPath = toGeneralPathImpl(path);
        final ExtendedAttributeView view = gPath.getAttrView(name);
        if (view == null && name.equals("basic")) {
            return createFileAttributeView(gPath, SimpleBasicFileAttributeView.class);
        } else {
            return view;
        }
    }

    @SuppressWarnings("unchecked")
    @Override
    public <A extends BasicFileAttributes> A readAttributes(final Path path,
                                                            final Class<A> type,
                                                            final LinkOption... options)
            throws NoSuchFileException, UnsupportedOperationException, IOException, SecurityException {
        checkNotNull("path",
                     path);
        checkNotNull("type",
                     type);

        checkFileNotExistThenThrow(path, false);

        if (type == BasicFileAttributesImpl.class || type == BasicFileAttributes.class) {
            final SimpleBasicFileAttributeView view = getFileAttributeView(path,
                                                                           SimpleBasicFileAttributeView.class,
                                                                           options);
            return (A) view.readAttributes();
        }

        return null;
    }

    @Override
    public Map<String, Object> readAttributes(final Path path,
                                              final String attributes,
                                              final LinkOption... options)
            throws UnsupportedOperationException, IllegalArgumentException, IOException, SecurityException {
        checkNotNull("path",
                     path);
        checkNotEmpty("attributes",
                      attributes);

        final String[] s = split(attributes);
        if (s[0].length() == 0) {
            throw new IllegalArgumentException(attributes);
        }

        final ExtendedAttributeView view = getFileAttributeView(path,
                                                                s[0],
                                                                options);
        if (view == null) {
            throw new UnsupportedOperationException("View '" + s[0] + "' not available");
        }
        return view.readAttributes(s[1].split(","));
    }

    @Override
    public void setAttribute(final Path path,
                             final String attribute,
                             final Object value,
                             final LinkOption... options)
            throws UnsupportedOperationException, IllegalArgumentException, ClassCastException, IOException, SecurityException {
        checkNotNull("path",
                     path);
        checkNotEmpty("attributes",
                      attribute);

        final String[] s = split(attribute);
        if (s[0].length() == 0) {
            throw new IllegalArgumentException(attribute);
        }
        final ExtendedAttributeView view = getFileAttributeView(path,
                                                                s[0],
                                                                options);
        if (view == null) {
            throw new UnsupportedOperationException("View '" + s[0] + "' not available");
        }
        view.setAttribute(attribute,
                          value);
    }

    @Override
    public void copy(final Path source,
                     final Path target,
                     final CopyOption... options)
            throws UnsupportedOperationException, FileAlreadyExistsException, DirectoryNotEmptyException, IOException, SecurityException {
        checkNotNull("source",
                     source);
        checkNotNull("target",
                     target);
        checkCondition("source must exist",
                       source.toFile().exists());

        checkFileExistsThenThrow(target);
        if (source.toFile().isDirectory() && source.toFile().list().length > 0) {
            throw new DirectoryNotEmptyException(source.toString());
        }

        try {
            if (source.toFile().isDirectory()) {
                FileUtils.copyDirectory(source.toFile(),
                                        target.toFile());
            } else {
                FileUtils.copyFile(source.toFile(),
                                   target.toFile());
            }
        } catch (java.io.IOException ex) {
            throw new IOException(ex);
        }
    }

    @Override
    public void move(final Path source,
                     final Path target,
                     final CopyOption... options)
            throws DirectoryNotEmptyException, AtomicMoveNotSupportedException, IOException, SecurityException {
        checkNotNull("source",
                     source);
        checkNotNull("target",
                     target);
        checkCondition("source must exist",
                       source.toFile().exists());

        checkFileExistsThenThrow(target);

        if (source.toFile().isDirectory() && source.toFile().list().length > 0) {
            throw new DirectoryNotEmptyException(source.toString());
        }

        try {
            if (source.toFile().isDirectory()) {
                FileUtils.moveDirectory(source.toFile(),
                                        target.toFile());
            } else {
                FileUtils.moveFile(source.toFile(),
                                   target.toFile());
            }
        } catch (java.io.IOException ex) {
            throw new IOException(ex);
        }
    }

    protected FileSystem getDefaultFileSystem() {
        return fileSystem;
    }

    protected GeneralPathImpl toGeneralPathImpl(final Path path) {
        if (path instanceof GeneralPathImpl) {
            return (GeneralPathImpl) path;
        }
        return GeneralPathImpl.create(fileSystem,
                                      path.toString(),
                                      false);
    }

    protected String[] split(final String attribute) {
        final String[] s = new String[2];
        final int pos = attribute.indexOf(':');
        if (pos == -1) {
            s[0] = "basic";
            s[1] = attribute;
        } else {
            s[0] = attribute.substring(0,
                                       pos);
            s[1] = (pos == attribute.length()) ? "" : attribute.substring(pos + 1);
        }
        return s;
    }

    protected void checkFileNotExistThenThrow(final Path path, final boolean isLink) {
        if (!path.toFile().exists()) {
            if (isLink) {
                throw new NotLinkException(path.toString());
            } else {
                throw new NoSuchFileException(path.toString());
            }
        }
    }

    protected void checkFileExistsThenThrow(final Path path) {
        if (path.toFile().exists()) {
            throw new FileAlreadyExistsException(path.toString());
        }
    }

    public enum OSType {
        WINDOWS,
        UNIX_LIKE;

        public static OSType currentOS() {
            if (System.getProperty("os.name").toLowerCase().indexOf("win") >= 0) {
                return WINDOWS;
            }
            return UNIX_LIKE;
        }
    }
}
